{
  "cells": [
    {
      "metadata": {
        "id": "MpkYHwCqk7W-"
      },
      "cell_type": "markdown",
      "source": [
        "# **MuJoCo** tutorial with `dm_control` Python bindings\n",
        "\n",
        "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/google-deepmind/dm_control/blob/main/dm_control/mujoco/tutorial.ipynb)\n",
        "\n",
        "\n",
        "\n",
        "\n",
        "\n",
        "\n",
        "\n",
        "\n"
      ]
    },
    {
      "metadata": {
        "id": "_UbO9uhtBSX5"
      },
      "cell_type": "markdown",
      "source": [
        "\u003e \u003cp\u003e\u003csmall\u003e\u003csmall\u003eCopyright 2021 The dm_control Authors.\u003c/small\u003e\u003c/p\u003e\n",
        "\u003e \u003cp\u003e\u003csmall\u003e\u003csmall\u003eLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at \u003ca href=\"http://www.apache.org/licenses/LICENSE-2.0\"\u003ehttp://www.apache.org/licenses/LICENSE-2.0\u003c/a\u003e.\u003c/small\u003e\u003c/small\u003e\u003c/p\u003e\n",
        "\u003e \u003cp\u003e\u003csmall\u003e\u003csmall\u003eUnless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.\u003c/small\u003e\u003c/small\u003e\u003c/p\u003e"
      ]
    },
    {
      "metadata": {
        "id": "aThGKGp0cD76"
      },
      "cell_type": "markdown",
      "source": [
        "This notebook provides an overview tutorial of the **MuJoCo** physics simulator, using the `dm_control` Python bindings. It is similar to the notebook in `dm_control/tutorial.ipynb`, but focuses on teaching MuJoCo itself, rather than the additional features provided by the Python package. \n",
        "\n",
        "**A Colab runtime with GPU acceleration is required.** If you're using a CPU-only runtime, you can switch using the menu \"Runtime \u003e Change runtime type\"."
      ]
    },
    {
      "metadata": {
        "id": "YkBQUjm6gbGF"
      },
      "cell_type": "markdown",
      "source": [
        "<!-- Internal installation instructions. -->"
      ]
    },
    {
      "metadata": {
        "id": "YvyGCsgSCxHQ"
      },
      "cell_type": "markdown",
      "source": [
        "### Installing `dm_control` on Colab"
      ]
    },
    {
      "metadata": {
        "id": "IbZxYDxzoz5R"
      },
      "cell_type": "code",
      "source": [
        "#@title Run to install MuJoCo and `dm_control`\n",
        "import distutils.util\n",
        "import os\n",
        "import subprocess\n",
        "if subprocess.run('nvidia-smi').returncode:\n",
        "  raise RuntimeError(\n",
        "      'Cannot communicate with GPU. '\n",
        "      'Make sure you are using a GPU Colab runtime. '\n",
        "      'Go to the Runtime menu and select Choose runtime type.')\n",
        "\n",
        "# Add an ICD config so that glvnd can pick up the Nvidia EGL driver.\n",
        "# This is usually installed as part of an Nvidia driver package, but the Colab\n",
        "# kernel doesn't install its driver via APT, and as a result the ICD is missing.\n",
        "# (https://github.com/NVIDIA/libglvnd/blob/master/src/EGL/icd_enumeration.md)\n",
        "NVIDIA_ICD_CONFIG_PATH = '/usr/share/glvnd/egl_vendor.d/10_nvidia.json'\n",
        "if not os.path.exists(NVIDIA_ICD_CONFIG_PATH):\n",
        "  with open(NVIDIA_ICD_CONFIG_PATH, 'w') as f:\n",
        "    f.write(\"\"\"{\n",
        "    \"file_format_version\" : \"1.0.0\",\n",
        "    \"ICD\" : {\n",
        "        \"library_path\" : \"libEGL_nvidia.so.0\"\n",
        "    }\n",
        "}\n",
        "\"\"\")\n",
        "\n",
        "print('Installing dm_control...')\n",
        "!pip install -q dm_control\u003e=1.0.35\n",
        "\n",
        "# Configure dm_control to use the EGL rendering backend (requires GPU)\n",
        "%env MUJOCO_GL=egl\n",
        "\n",
        "print('Checking that the dm_control installation succeeded...')\n",
        "try:\n",
        "  from dm_control import suite\n",
        "  env = suite.load('cartpole', 'swingup')\n",
        "  pixels = env.physics.render()\n",
        "except Exception as e:\n",
        "  raise e from RuntimeError(\n",
        "      'Something went wrong during installation. Check the shell output above '\n",
        "      'for more information.\\n'\n",
        "      'If using a hosted Colab runtime, make sure you enable GPU acceleration '\n",
        "      'by going to the Runtime menu and selecting \"Choose runtime type\".')\n",
        "else:\n",
        "  del suite, pixels\n",
        "print('dm_control installation succeeded.')"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "wtDN43hIJh2C"
      },
      "cell_type": "markdown",
      "source": [
        "# Imports\n",
        "\n",
        "Run both of these cells:"
      ]
    },
    {
      "metadata": {
        "id": "T5f4w3Kq2X14"
      },
      "cell_type": "code",
      "source": [
        "#@title All `dm_control` imports required for this tutorial\n",
        "\n",
        "# The basic mujoco wrapper.\n",
        "from dm_control import mujoco\n",
        "\n",
        "# Access to enums and MuJoCo library functions.\n",
        "from dm_control.mujoco.wrapper.mjbindings import enums\n",
        "from dm_control.mujoco.wrapper.mjbindings import mjlib\n",
        "\n",
        "# Composer high level imports\n",
        "from dm_control import composer\n",
        "from dm_control.composer.observation import observable\n",
        "from dm_control.composer import variation\n",
        "\n",
        "# Imports for Composer tutorial example\n",
        "from dm_control.composer.variation import distributions\n",
        "from dm_control.composer.variation import noises\n",
        "from dm_control.locomotion.arenas import floors\n",
        "\n",
        "# Control Suite\n",
        "from dm_control import suite\n",
        "\n",
        "# Run through corridor example\n",
        "from dm_control.locomotion.walkers import cmu_humanoid\n",
        "from dm_control.locomotion.arenas import corridors as corridor_arenas\n",
        "from dm_control.locomotion.tasks import corridors as corridor_tasks\n",
        "\n",
        "# Soccer\n",
        "from dm_control.locomotion import soccer\n",
        "\n",
        "# Manipulation\n",
        "from dm_control import manipulation"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "gKc1FNhKiVJX"
      },
      "cell_type": "code",
      "source": [
        "#@title Other imports and helper functions\n",
        "\n",
        "# General\n",
        "import copy\n",
        "import os\n",
        "import time\n",
        "import itertools\n",
        "from IPython.display import clear_output\n",
        "import numpy as np\n",
        "\n",
        "# Graphics-related\n",
        "import matplotlib\n",
        "import matplotlib.animation as animation\n",
        "import matplotlib.pyplot as plt\n",
        "from IPython.display import HTML\n",
        "import PIL.Image\n",
        "# Internal loading of video libraries.\n",
        "\n",
        "# Use svg backend for figure rendering\n",
        "%config InlineBackend.figure_format = 'svg'\n",
        "\n",
        "# Font sizes\n",
        "SMALL_SIZE = 8\n",
        "MEDIUM_SIZE = 10\n",
        "BIGGER_SIZE = 12\n",
        "plt.rc('font', size=SMALL_SIZE)          # controls default text sizes\n",
        "plt.rc('axes', titlesize=SMALL_SIZE)     # fontsize of the axes title\n",
        "plt.rc('axes', labelsize=MEDIUM_SIZE)    # fontsize of the x and y labels\n",
        "plt.rc('xtick', labelsize=SMALL_SIZE)    # fontsize of the tick labels\n",
        "plt.rc('ytick', labelsize=SMALL_SIZE)    # fontsize of the tick labels\n",
        "plt.rc('legend', fontsize=SMALL_SIZE)    # legend fontsize\n",
        "plt.rc('figure', titlesize=BIGGER_SIZE)  # fontsize of the figure title\n",
        "\n",
        "# Inline video helper function\n",
        "if os.environ.get('COLAB_NOTEBOOK_TEST', False):\n",
        "  # We skip video generation during tests, as it is quite expensive.\n",
        "  display_video = lambda *args, **kwargs: None\n",
        "else:\n",
        "  def display_video(frames, framerate=30):\n",
        "    height, width, _ = frames[0].shape\n",
        "    dpi = 70\n",
        "    orig_backend = matplotlib.get_backend()\n",
        "    matplotlib.use('Agg')  # Switch to headless 'Agg' to inhibit figure rendering.\n",
        "    fig, ax = plt.subplots(1, 1, figsize=(width / dpi, height / dpi), dpi=dpi)\n",
        "    matplotlib.use(orig_backend)  # Switch back to the original backend.\n",
        "    ax.set_axis_off()\n",
        "    ax.set_aspect('equal')\n",
        "    ax.set_position([0, 0, 1, 1])\n",
        "    im = ax.imshow(frames[0])\n",
        "    def update(frame):\n",
        "      im.set_data(frame)\n",
        "      return [im]\n",
        "    interval = 1000/framerate\n",
        "    anim = animation.FuncAnimation(fig=fig, func=update, frames=frames,\n",
        "                                   interval=interval, blit=True, repeat=False)\n",
        "    return HTML(anim.to_html5_video())\n",
        "\n",
        "# Seed numpy's global RNG so that cell outputs are deterministic. We also try to\n",
        "# use RandomState instances that are local to a single cell wherever possible.\n",
        "np.random.seed(42)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "jZXz9rPYGA-Y"
      },
      "cell_type": "markdown",
      "source": [
        "# Model definition, compilation and rendering\n",
        "\n"
      ]
    },
    {
      "metadata": {
        "id": "MRBaZsf1d7Gb"
      },
      "cell_type": "markdown",
      "source": [
        "We begin by describing some basic concepts of the [MuJoCo](http://mujoco.org/) physics simulation library, but recommend the [official documentation](http://mujoco.org/book/) for details.\n",
        "\n",
        "Let's define a simple model with two geoms and a light."
      ]
    },
    {
      "metadata": {
        "id": "ZS2utl59ZTsr"
      },
      "cell_type": "code",
      "source": [
        "#@title A static model {vertical-output: true}\n",
        "\n",
        "static_model = \"\"\"\n",
        "\u003cmujoco\u003e\n",
        "  \u003cworldbody\u003e\n",
        "    \u003clight name=\"top\" pos=\"0 0 1\"/\u003e\n",
        "    \u003cgeom name=\"red_box\" type=\"box\" size=\".2 .2 .2\" rgba=\"1 0 0 1\"/\u003e\n",
        "    \u003cgeom name=\"green_sphere\" pos=\".2 .2 .2\" size=\".1\" rgba=\"0 1 0 1\"/\u003e\n",
        "  \u003c/worldbody\u003e\n",
        "\u003c/mujoco\u003e\n",
        "\"\"\"\n",
        "physics = mujoco.Physics.from_xml_string(static_model)\n",
        "pixels = physics.render()\n",
        "PIL.Image.fromarray(pixels)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "p4vPllljTJh8"
      },
      "cell_type": "markdown",
      "source": [
        "`static_model` is written in MuJoCo's XML-based [MJCF](http://www.mujoco.org/book/modeling.html) modeling language. The `from_xml_string()` method invokes the model compiler, which instantiates the library's internal data structures. These can be accessed via the `physics` object, see below."
      ]
    },
    {
      "metadata": {
        "id": "MdUF2UYmR4TA"
      },
      "cell_type": "markdown",
      "source": [
        "## Adding DOFs and simulating, advanced rendering\n",
        "This is a perfectly legitimate model, but if we simulate it, nothing will happen except for time advancing. This is because this model has no degrees of freedom (DOFs). We add DOFs by adding **joints** to bodies, specifying how they can move with respect to their parents. Let us add a hinge joint and re-render, visualizing the joint axis."
      ]
    },
    {
      "metadata": {
        "id": "R7zokzd_yeEg"
      },
      "cell_type": "code",
      "source": [
        "#@title A child body with a joint { vertical-output: true }\n",
        "\n",
        "swinging_body = \"\"\"\n",
        "\u003cmujoco\u003e\n",
        "  \u003cworldbody\u003e\n",
        "    \u003clight name=\"top\" pos=\"0 0 1\"/\u003e\n",
        "    \u003cbody name=\"box_and_sphere\" euler=\"0 0 -30\"\u003e\n",
        "      \u003cjoint name=\"swing\" type=\"hinge\" axis=\"1 -1 0\" pos=\"-.2 -.2 -.2\"/\u003e\n",
        "      \u003cgeom name=\"red_box\" type=\"box\" size=\".2 .2 .2\" rgba=\"1 0 0 1\"/\u003e\n",
        "      \u003cgeom name=\"green_sphere\" pos=\".2 .2 .2\" size=\".1\" rgba=\"0 1 0 1\"/\u003e\n",
        "    \u003c/body\u003e\n",
        "  \u003c/worldbody\u003e\n",
        "\u003c/mujoco\u003e\n",
        "\"\"\"\n",
        "physics = mujoco.Physics.from_xml_string(swinging_body)\n",
        "# Visualize the joint axis.\n",
        "scene_option = mujoco.wrapper.core.MjvOption()\n",
        "scene_option.flags[enums.mjtVisFlag.mjVIS_JOINT] = True\n",
        "pixels = physics.render(scene_option=scene_option)\n",
        "PIL.Image.fromarray(pixels)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "INOGGV0PTQus"
      },
      "cell_type": "markdown",
      "source": [
        "The things that move (and which have inertia) are called *bodies*. The body's child `joint` specifies how that body can move with respect to its parent, in this case `box_and_sphere` with respect to the `worldbody`. \n",
        "\n",
        "Note that the body's frame is **rotated** with an `euler` directive, and its children, the geoms and the joint, rotate with it. This is to emphasize the local-to-parent-frame nature of position and orientation directives in MJCF.\n",
        "\n",
        "Let's make a video, to get a sense of the dynamics and to see the body swinging under gravity."
      ]
    },
    {
      "metadata": {
        "id": "Z_57VMUDpGrj"
      },
      "cell_type": "code",
      "source": [
        "#@title Making a video {vertical-output: true}\n",
        "\n",
        "duration = 2    # (seconds)\n",
        "framerate = 30  # (Hz)\n",
        "\n",
        "# Visualize the joint axis\n",
        "scene_option = mujoco.wrapper.core.MjvOption()\n",
        "scene_option.flags[enums.mjtVisFlag.mjVIS_JOINT] = True\n",
        "\n",
        "# Simulate and display video.\n",
        "frames = []\n",
        "physics.reset()  # Reset state and time\n",
        "while physics.data.time \u003c duration:\n",
        "  physics.step()\n",
        "  if len(frames) \u003c physics.data.time * framerate:\n",
        "    pixels = physics.render(scene_option=scene_option)\n",
        "    frames.append(pixels)\n",
        "display_video(frames, framerate)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "yYvS1UaciMX_"
      },
      "cell_type": "markdown",
      "source": [
        "Note how we collect the video frames. Because physics simulation timesteps are generally much smaller than framerates (the default timestep is 2ms), we don't render after each step."
      ]
    },
    {
      "metadata": {
        "id": "nQ8XOnRQx7T1"
      },
      "cell_type": "markdown",
      "source": [
        "## Rendering options\n",
        "\n",
        "Like joint visualisation, additional rendering options are exposed as parameters to the `render` method."
      ]
    },
    {
      "metadata": {
        "id": "AQITZiIgx7T2"
      },
      "cell_type": "code",
      "source": [
        "#@title Enable transparency and frame visualization {vertical-output: true}\n",
        "\n",
        "scene_option = mujoco.wrapper.core.MjvOption()\n",
        "scene_option.frame = enums.mjtFrame.mjFRAME_GEOM\n",
        "scene_option.flags[enums.mjtVisFlag.mjVIS_TRANSPARENT] = True\n",
        "pixels = physics.render(scene_option=scene_option)\n",
        "PIL.Image.fromarray(pixels)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "PDDgY48vx7T6"
      },
      "cell_type": "code",
      "source": [
        "#@title Depth rendering {vertical-output: true}\n",
        "\n",
        "# depth is a float array, in meters.\n",
        "depth = physics.render(depth=True)\n",
        "# Shift nearest values to the origin.\n",
        "depth -= depth.min()\n",
        "# Scale by 2 mean distances of near rays.\n",
        "depth /= 2*depth[depth \u003c= 1].mean()\n",
        "# Scale to [0, 255]\n",
        "pixels = 255*np.clip(depth, 0, 1)\n",
        "PIL.Image.fromarray(pixels.astype(np.uint8))"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "PNwiIrgpx7T8"
      },
      "cell_type": "code",
      "source": [
        "#@title Segmentation rendering {vertical-output: true}\n",
        "\n",
        "seg = physics.render(segmentation=True)\n",
        "# Display the contents of the first channel, which contains object\n",
        "# IDs. The second channel, seg[:, :, 1], contains object types.\n",
        "geom_ids = seg[:, :, 0]\n",
        "# Infinity is mapped to -1\n",
        "geom_ids = geom_ids.astype(np.float64) + 1\n",
        "# Scale to [0, 1]\n",
        "geom_ids = geom_ids / geom_ids.max()\n",
        "pixels = 255*geom_ids\n",
        "PIL.Image.fromarray(pixels.astype(np.uint8))\n"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "uCJQlv3cQcJQ"
      },
      "cell_type": "code",
      "source": [
        "#@title Projecting from world to camera coordinates {vertical-output: true}\n",
        "\n",
        "# Get the world coordinates of the box corners\n",
        "box_pos = physics.named.data.geom_xpos['red_box']\n",
        "box_mat = physics.named.data.geom_xmat['red_box'].reshape(3, 3)\n",
        "box_size = physics.named.model.geom_size['red_box']\n",
        "offsets = np.array([-1, 1]) * box_size[:, None]\n",
        "xyz_local = np.stack(list(itertools.product(*offsets))).T\n",
        "xyz_global = box_pos[:, None] + box_mat @ xyz_local\n",
        "\n",
        "# Camera matrices multiply homogenous [x, y, z, 1] vectors.\n",
        "corners_homogeneous = np.ones((4, xyz_global.shape[1]), dtype=float)\n",
        "corners_homogeneous[:3, :] = xyz_global\n",
        "\n",
        "# Get the camera matrix.\n",
        "camera = mujoco.Camera(physics)\n",
        "camera_matrix = camera.matrix\n",
        "\n",
        "# Project world coordinates into pixel space. See:\n",
        "# https://en.wikipedia.org/wiki/3D_projection#Mathematical_formula\n",
        "xs, ys, s = camera_matrix @ corners_homogeneous\n",
        "# x and y are in the pixel coordinate system.\n",
        "x = xs / s\n",
        "y = ys / s\n",
        "\n",
        "# Render the camera view and overlay the projected corner coordinates.\n",
        "pixels = camera.render()\n",
        "fig, ax = plt.subplots(1, 1)\n",
        "ax.imshow(pixels)\n",
        "ax.plot(x, y, '+', c='w')\n",
        "ax.set_axis_off()"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "R_jkaXd_9Eiu"
      },
      "cell_type": "code",
      "source": [
        "#@title Adding arbitrary 3D geometry {vertical-output: true}\n",
        "\n",
        "def get_geom_speed(physics, geom_name):\n",
        "  \"\"\"Returns the speed of a geom.\"\"\"\n",
        "  geom_vel = np.zeros(6)\n",
        "  geom_type = mujoco.mjtObj.mjOBJ_GEOM\n",
        "  geom_id = mujoco.mj_name2id(physics.model.ptr, geom_type, geom_name)\n",
        "  mujoco.mj_objectVelocity(physics.model.ptr, physics.data.ptr,\n",
        "                           geom_type, geom_id, geom_vel, 0)\n",
        "  return np.linalg.norm(geom_vel)\n",
        "\n",
        "def add_visual_capsule(scene, point1, point2, radius, rgba):\n",
        "  \"\"\"Adds one capsule to an mjvScene.\"\"\"\n",
        "  if scene.ngeom \u003e= scene.maxgeom:\n",
        "    return\n",
        "  scene.ngeom += 1  # increment ngeom\n",
        "  # initialise a new capsule, add it to the scene using mjv_connector\n",
        "  mujoco.mjv_initGeom(scene.geoms[scene.ngeom-1],\n",
        "                      mujoco.mjtGeom.mjGEOM_CAPSULE, np.zeros(3),\n",
        "                      np.zeros(3), np.zeros(9), rgba.astype(np.float32))\n",
        "  mujoco.mjv_connector(scene.geoms[scene.ngeom-1],\n",
        "                       mujoco.mjtGeom.mjGEOM_CAPSULE, radius,\n",
        "                       point1, point2)\n",
        "\n",
        " # traces of time, position and speed\n",
        "times = []\n",
        "positions = []\n",
        "speeds = []\n",
        "offset = physics.model.jnt_axis[0]/8  # offset along the joint axis\n",
        "\n",
        "def scene_callback(physics, scn):\n",
        "  \"\"\"Draw position trace, speed modifies width and colours.\"\"\"\n",
        "  if len(positions) \u003e 1:\n",
        "    for i in range(len(positions)-1):\n",
        "      rgba=np.array((np.clip(speeds[i]/10, 0, 1),\n",
        "                     np.clip(1-speeds[i]/10, 0, 1),\n",
        "                     .5, 1.))\n",
        "      radius=.003*(1+speeds[i])\n",
        "      point1 = positions[i] + offset*times[i]\n",
        "      point2 = positions[i+1] + offset*times[i+1]\n",
        "      add_visual_capsule(scn, point1, point2, radius, rgba)\n",
        "\n",
        "duration = 6    # (seconds)\n",
        "framerate = 30  # (Hz)\n",
        "\n",
        "# Simulate and display video.\n",
        "frames = []\n",
        "physics.reset()  # Reset state and time\n",
        "while physics.data.time \u003c duration:\n",
        "  # append data to the traces\n",
        "  positions.append(physics.named.data.geom_xpos[\"green_sphere\"].copy())\n",
        "  times.append(physics.data.time)\n",
        "  speeds.append(get_geom_speed(physics, \"green_sphere\"))\n",
        "  physics.step()\n",
        "  if len(frames) \u003c physics.data.time * framerate:\n",
        "    camera = mujoco.Camera(physics, max_geom=10000,\n",
        "                           scene_callback=scene_callback)\n",
        "    pixels = camera.render()\n",
        "    frames.append(pixels)\n",
        "display_video(frames, framerate)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "gf9h_wi9weet"
      },
      "cell_type": "markdown",
      "source": [
        "# MuJoCo basics and named indexing"
      ]
    },
    {
      "metadata": {
        "id": "NCcZxrDDB1Cj"
      },
      "cell_type": "markdown",
      "source": [
        "## `mjModel`\n",
        "MuJoCo's `mjModel`, encapsulated in `physics.model`, contains the *model description*, including the default initial state and other fixed quantities which are not a function of the state, e.g. the positions of geoms in the frame of their parent body. The (x, y, z) offsets of the `box` and `sphere` geoms, relative their parent body `box_and_sphere` are given by `model.geom_pos`:"
      ]
    },
    {
      "metadata": {
        "id": "wx8NANvOF8g1"
      },
      "cell_type": "code",
      "source": [
        "physics.model.geom_pos"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "v9O1VNdmHn_K"
      },
      "cell_type": "markdown",
      "source": [
        "Docstrings of attributes provide short descriptions."
      ]
    },
    {
      "metadata": {
        "id": "8_0MwaeYHn_N"
      },
      "cell_type": "code",
      "source": [
        "help(type(physics.model).geom_pos)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "Wee5ATLtIQn_"
      },
      "cell_type": "markdown",
      "source": [
        "The `model.opt` structure contains global quantities like"
      ]
    },
    {
      "metadata": {
        "id": "BhzbZIfDIU2-"
      },
      "cell_type": "code",
      "source": [
        "print('timestep', physics.model.opt.timestep)\n",
        "print('gravity', physics.model.opt.gravity)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "t5hY0fyXFLcf"
      },
      "cell_type": "markdown",
      "source": [
        "## `mjData`\n",
        "`mjData`, encapsulated in `physics.data`, contains the **state** and quantities that depend on it. The state is made up of time, generalized positions and generalised velocities. These are respectively `data.time`, `data.qpos` and `data.qvel`. \n",
        "\n",
        "Let's print the state of the swinging body where we left it:"
      ]
    },
    {
      "metadata": {
        "id": "acwZtDwp9mQU"
      },
      "cell_type": "code",
      "source": [
        "print(physics.data.time, physics.data.qpos, physics.data.qvel)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "7YlmcLcA-WQu"
      },
      "cell_type": "markdown",
      "source": [
        "`physics.data` also contains **functions of the state**, for example the cartesian positions of objects in the world frame. The (x, y, z) positions of our two geoms are in `data.geom_xpos`:"
      ]
    },
    {
      "metadata": {
        "id": "CPwDcAQ0-uUE"
      },
      "cell_type": "code",
      "source": [
        "print(physics.data.geom_xpos)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "Z0UodCxS_v49"
      },
      "cell_type": "markdown",
      "source": [
        "## Named indexing\n",
        "\n",
        "The semantics of the above arrays are made clearer using the `named` wrapper, which assigns names to rows and type names to columns."
      ]
    },
    {
      "metadata": {
        "id": "cLARcaK6-xCU"
      },
      "cell_type": "code",
      "source": [
        "print(physics.named.data.geom_xpos)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "wgXOUZNZHIx6"
      },
      "cell_type": "markdown",
      "source": [
        "Note how `model.geom_pos` and `data.geom_xpos` have similar semantics but very different meanings."
      ]
    },
    {
      "metadata": {
        "id": "-cW61ClRHS8a"
      },
      "cell_type": "code",
      "source": [
        "print(physics.named.model.geom_pos)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "-lQ0AChVASMv"
      },
      "cell_type": "markdown",
      "source": [
        "Name strings can be used to index **into** the relevant quantities, making code much more readable and robust."
      ]
    },
    {
      "metadata": {
        "id": "Rj4ad9fQAnFZ"
      },
      "cell_type": "code",
      "source": [
        "physics.named.data.geom_xpos['green_sphere', 'z']"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "axr_p6APAzFn"
      },
      "cell_type": "markdown",
      "source": [
        "Joint names can be used to index into quantities in joint space (beginning with the letter `q`):"
      ]
    },
    {
      "metadata": {
        "id": "hluF9aDG9O1W"
      },
      "cell_type": "code",
      "source": [
        "physics.named.data.qpos['swing']"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "3IhfyD2Q1pjv"
      },
      "cell_type": "markdown",
      "source": [
        "We can mix NumPy slicing operations with named indexing. As an example, we can set the color of the box using its name (`\"red_box\"`) as an index into the rows of the `geom_rgba` array. "
      ]
    },
    {
      "metadata": {
        "id": "f5vVUullUvWH"
      },
      "cell_type": "code",
      "source": [
        "#@title Changing colors using named indexing{vertical-output: true}\n",
        "\n",
        "random_rgb = np.random.rand(3)\n",
        "physics.named.model.geom_rgba['red_box', :3] = random_rgb\n",
        "pixels = physics.render()\n",
        "PIL.Image.fromarray(pixels)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "elzPPdq-KhLI"
      },
      "cell_type": "markdown",
      "source": [
        "Note that while `physics.model` quantities will not be changed by the engine, we can change them ourselves between steps."
      ]
    },
    {
      "metadata": {
        "id": "22ENjtVuhwsm"
      },
      "cell_type": "markdown",
      "source": [
        "## Setting the state with `reset_context()`\n",
        "\n",
        "In order for `data` quantities that are functions of the state to be in sync with the state, MuJoCo's `mj_step1()` needs to be called. This is facilitated by the `reset_context()` context, please see in-depth discussion in Section 2.1 of the `dm_control` [tech report](https://arxiv.org/abs/2006.12983)."
      ]
    },
    {
      "metadata": {
        "id": "WBPprCtWgXFN"
      },
      "cell_type": "code",
      "source": [
        "physics.named.data.qpos['swing'] = np.pi\n",
        "print('Without reset_context, spatial positions are not updated:',\n",
        "      physics.named.data.geom_xpos['green_sphere', ['z']])\n",
        "with physics.reset_context():\n",
        "  physics.named.data.qpos['swing'] = np.pi\n",
        "print('After reset_context, positions are up-to-date:',\n",
        "      physics.named.data.geom_xpos['green_sphere', ['z']])"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "SHppAOjvSupc"
      },
      "cell_type": "markdown",
      "source": [
        "## Free bodies: the self-inverting \"tippe-top\"\n",
        "\n",
        "A free body is a body with a `free` joint, with 6 movement DOFs: 3 translations and 3 rotations. We could give our `box_and_sphere` body a free joint and watch it fall, but let's look at something more interesting. A \"tippe top\" is a spinning toy which flips itself on its head ([Wikipedia](https://en.wikipedia.org/wiki/Tippe_top)). We model it as follows:"
      ]
    },
    {
      "metadata": {
        "id": "xasXQpVMjIwA"
      },
      "cell_type": "code",
      "source": [
        "#@title The \"tippe-top\" model{vertical-output: true}\n",
        "\n",
        "tippe_top = \"\"\"\n",
        "\u003cmujoco model=\"tippe top\"\u003e\n",
        "  \u003coption integrator=\"RK4\"/\u003e\n",
        "\n",
        "  \u003casset\u003e\n",
        "    \u003ctexture name=\"grid\" type=\"2d\" builtin=\"checker\" rgb1=\".1 .2 .3\"\n",
        "     rgb2=\".2 .3 .4\" width=\"300\" height=\"300\"/\u003e\n",
        "    \u003cmaterial name=\"grid\" texture=\"grid\" texrepeat=\"8 8\" reflectance=\".2\"/\u003e\n",
        "  \u003c/asset\u003e\n",
        "\n",
        "  \u003cworldbody\u003e\n",
        "    \u003cgeom size=\".2 .2 .01\" type=\"plane\" material=\"grid\"/\u003e\n",
        "    \u003clight pos=\"0 0 .6\"/\u003e\n",
        "    \u003ccamera name=\"closeup\" pos=\"0 -.1 .07\" xyaxes=\"1 0 0 0 1 2\"/\u003e\n",
        "    \u003cbody name=\"top\" pos=\"0 0 .02\"\u003e\n",
        "      \u003cfreejoint/\u003e\n",
        "      \u003cgeom name=\"ball\" type=\"sphere\" size=\".02\" /\u003e\n",
        "      \u003cgeom name=\"stem\" type=\"cylinder\" pos=\"0 0 .02\" size=\"0.004 .008\"/\u003e\n",
        "      \u003cgeom name=\"ballast\" type=\"box\" size=\".023 .023 0.005\"  pos=\"0 0 -.015\"\n",
        "       contype=\"0\" conaffinity=\"0\" group=\"3\"/\u003e\n",
        "    \u003c/body\u003e\n",
        "  \u003c/worldbody\u003e\n",
        "\n",
        "  \u003ckeyframe\u003e\n",
        "    \u003ckey name=\"spinning\" qpos=\"0 0 0.02 1 0 0 0\" qvel=\"0 0 0 0 1 200\" /\u003e\n",
        "  \u003c/keyframe\u003e\n",
        "\u003c/mujoco\u003e\n",
        "\"\"\"\n",
        "physics = mujoco.Physics.from_xml_string(tippe_top)\n",
        "PIL.Image.fromarray(physics.render(camera_id='closeup'))"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "bvHlr6maJYIG"
      },
      "cell_type": "markdown",
      "source": [
        "Note several new features of this model definition:\n",
        "0. The free joint is added with the `\u003cfreejoint/\u003e` clause, which is similar to `\u003cjoint type=\"free\"/\u003e`, but prohibits unphysical attributes like friction or stiffness.\n",
        "1. We use the `\u003coption/\u003e` clause to set the integrator to the more accurate Runge Kutta 4th order.\n",
        "2. We define the floor's grid material inside the `\u003casset/\u003e` clause and reference it in the floor geom. \n",
        "3. We use an invisible and non-colliding box geom called `ballast` to move the top's center-of-mass lower. Having a low center of mass is  (counter-intuitively) required for the flipping behaviour to occur.\n",
        "4. We save our initial spinning state as a keyframe. It has a high rotational velocity around the z-axis, but is not perfectly oriented with the world.\n",
        "5. We define a `\u003ccamera\u003e` in our model, and then render from it using the `camera_id` argument to `render()`.\n",
        "Let us examine the state:"
      ]
    },
    {
      "metadata": {
        "id": "o4S9nYhHOKmb"
      },
      "cell_type": "code",
      "source": [
        "print('positions', physics.data.qpos)\n",
        "print('velocities', physics.data.qvel)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "71UgzBAqWdtZ"
      },
      "cell_type": "markdown",
      "source": [
        "The velocities are easy to interpret, 6 zeros, one for each DOF. What about the length-7 positions? We can see the initial 2cm height of the body; the subsequent four numbers are the 3D orientation, defined by a *unit quaternion*. These normalized four-vectors, which preserve the topology of the orientation group, are the reason that `data.qpos` can be bigger than `data.qvel`: 3D orientations are represented with **4** numbers while angular velocities are **3** numbers."
      ]
    },
    {
      "metadata": {
        "id": "5P4HkhKNGQvs"
      },
      "cell_type": "code",
      "source": [
        "#@title Video of the tippe-top {vertical-output: true}\n",
        "\n",
        "duration = 7    # (seconds)\n",
        "framerate = 60  # (Hz)\n",
        "\n",
        "# Simulate and display video.\n",
        "frames = []\n",
        "physics.reset(0)  # Reset to keyframe 0 (load a saved state).\n",
        "while physics.data.time \u003c duration:\n",
        "  physics.step()\n",
        "  if len(frames) \u003c (physics.data.time) * framerate:\n",
        "    pixels = physics.render(camera_id='closeup')\n",
        "    frames.append(pixels)\n",
        "\n",
        "display_video(frames, framerate)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "rRuFKD2ubPgu"
      },
      "cell_type": "markdown",
      "source": [
        "### Measuring values from `physics.data`\n",
        "The `physics.data` structure contains all of the dynamic variables and intermediate results produced by the simulation. These are expected to change on each timestep. \n",
        "\n",
        "Below we simulate for 2000 timesteps and plot the state and height of the sphere as a function of time."
      ]
    },
    {
      "metadata": {
        "id": "1XXB6asJoZ2N"
      },
      "cell_type": "code",
      "source": [
        "#@title Measuring values {vertical-output: true}\n",
        "\n",
        "timevals = []\n",
        "angular_velocity = []\n",
        "stem_height = []\n",
        "\n",
        "# Simulate and save data\n",
        "physics.reset(0)\n",
        "while physics.data.time \u003c duration:\n",
        "  physics.step()\n",
        "  timevals.append(physics.data.time)\n",
        "  angular_velocity.append(physics.data.qvel[3:6].copy())\n",
        "  stem_height.append(physics.named.data.geom_xpos['stem', 'z'])\n",
        "\n",
        "dpi = 100\n",
        "width = 480\n",
        "height = 640\n",
        "figsize = (width / dpi, height / dpi)\n",
        "_, ax = plt.subplots(2, 1, figsize=figsize, dpi=dpi, sharex=True)\n",
        "\n",
        "ax[0].plot(timevals, angular_velocity)\n",
        "ax[0].set_title('angular velocity')\n",
        "ax[0].set_ylabel('radians / second')\n",
        "\n",
        "ax[1].plot(timevals, stem_height)\n",
        "ax[1].set_xlabel('time (seconds)')\n",
        "ax[1].set_ylabel('meters')\n",
        "_ = ax[1].set_title('stem height')"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "u_zN8vATwcGy"
      },
      "cell_type": "markdown",
      "source": [
        "# Example: A chaotic pendulum"
      ]
    },
    {
      "metadata": {
        "id": "g1MKUEL_eSCM"
      },
      "cell_type": "markdown",
      "source": [
        "Below is a model of a chaotic pendulum, similar to [this one](https://www.exploratorium.edu/exhibits/chaotic-pendulum) in the San Francisco Exploratorium. "
      ]
    },
    {
      "metadata": {
        "id": "3jHYTV-bwfrS"
      },
      "cell_type": "code",
      "source": [
        "#@title chaotic pendulum {vertical-output: true}\n",
        "chaotic_pendulum = \"\"\"\n",
        "\u003cmujoco\u003e\n",
        "\n",
        "  \u003coption timestep=\".001\" \u003e\n",
        "    \u003cflag energy=\"enable\" contact=\"disable\"/\u003e\n",
        "  \u003c/option\u003e\n",
        "\n",
        "  \u003cdefault\u003e\n",
        "    \u003cjoint type=\"hinge\" axis=\"0 -1 0\"/\u003e\n",
        "    \u003cgeom type=\"capsule\" size=\".02\"/\u003e\n",
        "  \u003c/default\u003e\n",
        "\n",
        "  \u003cworldbody\u003e\n",
        "    \u003clight pos=\"0 -.4 1\"/\u003e\n",
        "    \u003ccamera name=\"fixed\" pos=\"0 -1 0\" xyaxes=\"1 0 0 0 0 1\"/\u003e\n",
        "    \u003cbody name=\"0\" pos=\"0 0 .2\"\u003e\n",
        "      \u003cjoint name=\"root\"/\u003e\n",
        "      \u003cgeom fromto=\"-.2 0 0 .2 0 0\" rgba=\"1 1 0 1\"/\u003e\n",
        "      \u003cgeom fromto=\"0 0 0 0 0 -.25\" rgba=\"1 1 0 1\"/\u003e\n",
        "      \u003cbody name=\"1\" pos=\"-.2 0 0\"\u003e\n",
        "        \u003cjoint/\u003e\n",
        "        \u003cgeom fromto=\"0 0 0 0 0 -.2\" rgba=\"1 0 0 1\"/\u003e\n",
        "      \u003c/body\u003e\n",
        "      \u003cbody name=\"2\" pos=\".2 0 0\"\u003e\n",
        "        \u003cjoint/\u003e\n",
        "        \u003cgeom fromto=\"0 0 0 0 0 -.2\" rgba=\"0 1 0 1\"/\u003e\n",
        "      \u003c/body\u003e\n",
        "      \u003cbody name=\"3\" pos=\"0 0 -.25\"\u003e\n",
        "        \u003cjoint/\u003e\n",
        "        \u003cgeom fromto=\"0 0 0 0 0 -.2\" rgba=\"0 0 1 1\"/\u003e\n",
        "      \u003c/body\u003e\n",
        "    \u003c/body\u003e\n",
        "  \u003c/worldbody\u003e\n",
        "\n",
        "\u003c/mujoco\u003e\n",
        "\"\"\"\n",
        "physics = mujoco.Physics.from_xml_string(chaotic_pendulum)\n",
        "\n",
        "pixels = physics.render(height=480, width=640, camera_id=\"fixed\")\n",
        "PIL.Image.fromarray(pixels)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "EKZrTBSS5f49"
      },
      "cell_type": "markdown",
      "source": [
        "## Timing\n",
        "Let's see a video of it in action while we time the components:"
      ]
    },
    {
      "metadata": {
        "id": "-kNWvE9dNwYW"
      },
      "cell_type": "code",
      "source": [
        "#@title physics vs. rendering: {vertical-output: true}\n",
        "\n",
        "\n",
        "# setup\n",
        "n_seconds = 6\n",
        "framerate = 30  # Hz\n",
        "n_frames = int(n_seconds * framerate)\n",
        "frames = []\n",
        "\n",
        "# set initial state\n",
        "with physics.reset_context():\n",
        "  physics.named.data.qvel['root'] = 10\n",
        "\n",
        "# simulate and record frames\n",
        "frame = 0\n",
        "sim_time = 0\n",
        "render_time = 0\n",
        "n_steps = 0\n",
        "for i in range(n_frames):\n",
        "  while physics.data.time * framerate \u003c i:\n",
        "    tic = time.time()\n",
        "    physics.step()\n",
        "    sim_time += time.time() - tic\n",
        "    n_steps += 1\n",
        "  tic = time.time()\n",
        "  frame = physics.render(240, 320, camera_id=\"fixed\")\n",
        "  render_time += time.time() - tic\n",
        "  frames.append(frame.copy())\n",
        "\n",
        "# print timing and play video\n",
        "print('simulation: {:6.2f} ms/frame ({:5.0f}Hz)'.format(\n",
        "      1000*sim_time/n_steps, n_steps/sim_time))\n",
        "print('rendering:  {:6.2f} ms/frame ({:5.0f}Hz)'.format(\n",
        "      1000*render_time/n_frames, n_frames/render_time))\n",
        "print('\\n')\n",
        "\n",
        "# show video\n",
        "display_video(frames, framerate)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "Iqi_m8HT-X5k"
      },
      "cell_type": "markdown",
      "source": [
        "## Chaos\n",
        "This is a [chaotic](https://en.wikipedia.org/wiki/Chaos_theory) system, small pertubations in initial conditions accumulate quickly:"
      ]
    },
    {
      "metadata": {
        "id": "Pa_19EfvOzzg"
      },
      "cell_type": "code",
      "source": [
        "#@title chaos: sensitvity to pertubation {vertical-output: true}\n",
        "PERTURBATION = 1e-7\n",
        "SIM_DURATION = 10 # seconds\n",
        "NUM_REPEATS = 8\n",
        "\n",
        "# preallocate\n",
        "n_steps = int(SIM_DURATION / physics.model.opt.timestep)\n",
        "sim_time = np.zeros(n_steps)\n",
        "angle = np.zeros(n_steps)\n",
        "energy = np.zeros(n_steps)\n",
        "\n",
        "# prepare plotting axes\n",
        "_, ax = plt.subplots(2, 1, sharex=True)\n",
        "\n",
        "# simulate NUM_REPEATS times with slightly different initial conditions\n",
        "for _ in range(NUM_REPEATS):\n",
        "  # initialize\n",
        "  with physics.reset_context():\n",
        "    physics.data.qvel[0] = 10 # root joint velocity\n",
        "    # perturb initial velocities\n",
        "    physics.data.qvel[:] += PERTURBATION * np.random.randn(physics.model.nv)\n",
        "\n",
        "  # simulate\n",
        "  for i in range(n_steps):\n",
        "    physics.step()\n",
        "    sim_time[i] = physics.data.time\n",
        "    angle[i] = physics.named.data.qpos['root']\n",
        "    energy[i] = physics.data.energy[0] + physics.data.energy[1]\n",
        "\n",
        "  # plot\n",
        "  ax[0].plot(sim_time, angle)\n",
        "  ax[1].plot(sim_time, energy)\n",
        "\n",
        "# finalize plot\n",
        "ax[0].set_title('root angle')\n",
        "ax[0].set_ylabel('radian')\n",
        "ax[1].set_title('total energy')\n",
        "ax[1].set_ylabel('Joule')\n",
        "ax[1].set_xlabel('second')\n",
        "plt.tight_layout()"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "daSIA_ewFGxV"
      },
      "cell_type": "markdown",
      "source": [
        "## Timestep and accuracy\n",
        "__Q:__ Why is the energy varying at all? There is no friction or damping, this system should conserve energy. \n",
        "\n",
        "__A:__ Because of the discretization of time. \n",
        "\n",
        "If we decrease the timestep we'll get better accuracy, hence better energy conservation:"
      ]
    },
    {
      "metadata": {
        "id": "4z-7KN_fFme-"
      },
      "cell_type": "code",
      "source": [
        "#@title reducing the time-step: {vertical-output: true}\n",
        "SIM_DURATION = 10 # (seconds)\n",
        "TIMESTEPS = np.power(10, np.linspace(-2, -4, 5))\n",
        "\n",
        "# prepare plotting axes\n",
        "_, ax = plt.subplots(1, 1)\n",
        "\n",
        "for dt in TIMESTEPS:\n",
        "   # set timestep, print\n",
        "  physics.model.opt.timestep = dt\n",
        "\n",
        "  # allocate\n",
        "  n_steps = int(SIM_DURATION / physics.model.opt.timestep)\n",
        "  sim_time = np.zeros(n_steps)\n",
        "  energy = np.zeros(n_steps)\n",
        "\n",
        "  # initialize\n",
        "  with physics.reset_context():\n",
        "    physics.data.qvel[0] = 9 # root joint velocity\n",
        "\n",
        "  # simulate\n",
        "  print('{} steps at dt = {:2.2g}ms'.format(n_steps, 1000*dt))\n",
        "  for i in range(n_steps):\n",
        "    physics.step()\n",
        "    sim_time[i] = physics.data.time\n",
        "    energy[i] = physics.data.energy[0] + physics.data.energy[1]\n",
        "\n",
        "  # plot\n",
        "  ax.plot(sim_time, energy, label='timestep = {:2.2g}ms'.format(1000*dt))\n",
        "\n",
        "# finalize plot\n",
        "ax.set_title('energy')\n",
        "ax.set_ylabel('Joule')\n",
        "ax.set_xlabel('second')\n",
        "ax.legend(frameon=True);\n",
        "plt.tight_layout()"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "jsVkUm7QKb9I"
      },
      "cell_type": "markdown",
      "source": [
        "## Timestep and divergence\n",
        "When we increase the time step, the simulation quickly diverges"
      ]
    },
    {
      "metadata": {
        "id": "FbdUA4zDPbDP"
      },
      "cell_type": "code",
      "source": [
        "#@title increasing the time-step: {vertical-output: true}\n",
        "SIM_DURATION = 10 # (seconds)\n",
        "TIMESTEPS = np.power(10, np.linspace(-2, -1.5, 7))\n",
        "\n",
        "# get plotting axes\n",
        "ax = plt.gca()\n",
        "\n",
        "for dt in TIMESTEPS:\n",
        "   # set timestep\n",
        "  physics.model.opt.timestep = dt\n",
        "\n",
        "  # allocate\n",
        "  n_steps = int(SIM_DURATION / physics.model.opt.timestep)\n",
        "  sim_time = np.zeros(n_steps)\n",
        "  energy = np.zeros(n_steps) * np.nan\n",
        "  speed = np.zeros(n_steps) * np.nan\n",
        "\n",
        "  # initialize\n",
        "  with physics.reset_context():\n",
        "    physics.data.qvel[0] = 11 # root joint velocity\n",
        "\n",
        "  # simulate\n",
        "  print('{} steps at dt = {:2.2g}ms'.format(n_steps, 1000*dt))\n",
        "  for i in range(n_steps):\n",
        "    try:\n",
        "      physics.step()\n",
        "    except BaseException:  # raises mujoco.engine.base.PhysicsError\n",
        "      print('numerical divergence at timestep {}.'.format(i))\n",
        "      break\n",
        "    sim_time[i] = physics.data.time\n",
        "    energy[i] = sum(abs(physics.data.qvel))\n",
        "    speed[i] = np.linalg.norm(physics.data.qvel)\n",
        "\n",
        "  # plot\n",
        "  ax.plot(sim_time, energy, label='timestep = {:2.2g}ms'.format(1000*dt))\n",
        "  ax.set_yscale('log')\n",
        "\n",
        "\n",
        "# finalize plot\n",
        "ax.set_ybound(1, 1e3)\n",
        "ax.set_title('energy')\n",
        "ax.set_ylabel('Joule')\n",
        "ax.set_xlabel('second')\n",
        "ax.legend(frameon=True, loc='lower right');\n",
        "plt.tight_layout()\n"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "FITYfGyy3XPL"
      },
      "cell_type": "markdown",
      "source": [
        "# Contacts"
      ]
    },
    {
      "metadata": {
        "id": "2n1VNVv_FkbB"
      },
      "cell_type": "code",
      "source": [
        "#@title 'box_and_sphere' free body: {vertical-output: true}\n",
        "\n",
        "free_body_MJCF = \"\"\"\n",
        "\u003cmujoco\u003e\n",
        "  \u003casset\u003e\n",
        "    \u003ctexture name=\"grid\" type=\"2d\" builtin=\"checker\" rgb1=\".1 .2 .3\"\n",
        "    rgb2=\".2 .3 .4\" width=\"300\" height=\"300\" mark=\"edge\" markrgb=\".2 .3 .4\"/\u003e\n",
        "    \u003cmaterial name=\"grid\" texture=\"grid\" texrepeat=\"2 2\" texuniform=\"true\"\n",
        "    reflectance=\".2\"/\u003e\n",
        "  \u003c/asset\u003e\n",
        "\n",
        "  \u003cworldbody\u003e\n",
        "    \u003clight pos=\"0 0 1\" mode=\"trackcom\"/\u003e\n",
        "    \u003cgeom name=\"ground\" type=\"plane\" pos=\"0 0 -.5\" size=\"2 2 .1\" material=\"grid\" solimp=\".99 .99 .01\" solref=\".001 1\"/\u003e\n",
        "    \u003cbody name=\"box_and_sphere\" pos=\"0 0 0\"\u003e\n",
        "      \u003cfreejoint/\u003e\n",
        "      \u003cgeom name=\"red_box\" type=\"box\" size=\".1 .1 .1\" rgba=\"1 0 0 1\" solimp=\".99 .99 .01\"  solref=\".001 1\"/\u003e\n",
        "      \u003cgeom name=\"green_sphere\" size=\".06\" pos=\".1 .1 .1\" rgba=\"0 1 0 1\"/\u003e\n",
        "      \u003ccamera name=\"fixed\" pos=\"0 -.6 .3\" xyaxes=\"1 0 0 0 1 2\"/\u003e\n",
        "      \u003ccamera name=\"track\" pos=\"0 -.6 .3\" xyaxes=\"1 0 0 0 1 2\" mode=\"track\"/\u003e\n",
        "    \u003c/body\u003e\n",
        "  \u003c/worldbody\u003e\n",
        "\u003c/mujoco\u003e\n",
        "\"\"\"\n",
        "physics = mujoco.Physics.from_xml_string(free_body_MJCF)\n",
        "\n",
        "\n",
        "pixels = physics.render(400, 600, \"fixed\")\n",
        "PIL.Image.fromarray(pixels)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "HlRhFs_d3WLP"
      },
      "cell_type": "code",
      "source": [
        "#@title contacts in slow-motion: (0.25x){vertical-output: true}\n",
        "n_frames = 200\n",
        "height = 240\n",
        "width = 320\n",
        "frames = np.zeros((n_frames, height, width, 3), dtype=np.uint8)\n",
        "\n",
        "# visualize contact frames and forces, make body transparent\n",
        "options = mujoco.wrapper.core.MjvOption()\n",
        "mujoco.wrapper.core.mjlib.mjv_defaultOption(options.ptr)\n",
        "options.flags[enums.mjtVisFlag.mjVIS_CONTACTPOINT] = True\n",
        "options.flags[enums.mjtVisFlag.mjVIS_CONTACTFORCE] = True\n",
        "options.flags[enums.mjtVisFlag.mjVIS_TRANSPARENT] = True\n",
        "\n",
        "# tweak scales of contact visualization elements\n",
        "physics.model.vis.scale.contactwidth = 0.1\n",
        "physics.model.vis.scale.contactheight = 0.03\n",
        "physics.model.vis.scale.forcewidth = 0.05\n",
        "physics.model.vis.map.force = 0.3\n",
        "\n",
        "# random initial rotational velocity:\n",
        "with physics.reset_context():\n",
        "  physics.data.qvel[3:6] = 5*np.random.randn(3)\n",
        "\n",
        "# simulate and render\n",
        "for i in range(n_frames):\n",
        "  while physics.data.time \u003c i/120.0: #1/4x real time\n",
        "    physics.step()\n",
        "  frames[i] = physics.render(height, width, camera_id=\"track\", scene_option=options)\n",
        "# show video\n",
        "display_video(frames)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "_181TbtVSMBl"
      },
      "cell_type": "markdown",
      "source": [
        "## Analysis of contact forces"
      ]
    },
    {
      "metadata": {
        "id": "BMqyWeHki8Eg"
      },
      "cell_type": "code",
      "source": [
        "#@title contact-related quantities: {vertical-output: true}\n",
        "\n",
        "n_steps = 499\n",
        "\n",
        "# allocate\n",
        "sim_time = np.zeros(n_steps)\n",
        "ncon = np.zeros(n_steps)\n",
        "force = np.zeros((n_steps,3))\n",
        "velocity = np.zeros((n_steps, physics.model.nv))\n",
        "penetration = np.zeros(n_steps)\n",
        "acceleration = np.zeros((n_steps, physics.model.nv))\n",
        "forcetorque = np.zeros(6)\n",
        "\n",
        "# random initial rotational velocity:\n",
        "with physics.reset_context():\n",
        "  physics.data.qvel[3:6] = 2*np.random.randn(3)\n",
        "\n",
        "# simulate and save data\n",
        "for i in range(n_steps):\n",
        "  physics.step()\n",
        "  sim_time[i] = physics.data.time\n",
        "  ncon[i] = physics.data.ncon\n",
        "  velocity[i] = physics.data.qvel[:]\n",
        "  acceleration[i] = physics.data.qacc[:]\n",
        "  # iterate over active contacts, save force and distance\n",
        "  for j,c in enumerate(physics.data.contact):\n",
        "    mjlib.mj_contactForce(physics.model.ptr, physics.data.ptr,\n",
        "                          j, forcetorque)\n",
        "    force[i] += forcetorque[0:3]\n",
        "    penetration[i] = min(penetration[i], c.dist)\n",
        "  # we could also do\n",
        "  # force[i] += physics.data.qfrc_constraint[0:3]\n",
        "  # do you see why?\n",
        "\n",
        "# plot\n",
        "_, ax = plt.subplots(3, 2, sharex=True, figsize=(7, 10))\n",
        "\n",
        "lines = ax[0,0].plot(sim_time, force)\n",
        "ax[0,0].set_title('contact force')\n",
        "ax[0,0].set_ylabel('Newton')\n",
        "ax[0,0].legend(iter(lines), ('normal z', 'friction x', 'friction y'));\n",
        "\n",
        "ax[1,0].plot(sim_time, acceleration)\n",
        "ax[1,0].set_title('acceleration')\n",
        "ax[1,0].set_ylabel('(meter,radian)/s/s')\n",
        "\n",
        "ax[2,0].plot(sim_time, velocity)\n",
        "ax[2,0].set_title('velocity')\n",
        "ax[2,0].set_ylabel('(meter,radian)/s')\n",
        "ax[2,0].set_xlabel('second')\n",
        "\n",
        "ax[0,1].plot(sim_time, ncon)\n",
        "ax[0,1].set_title('number of contacts')\n",
        "ax[0,1].set_yticks(range(6))\n",
        "\n",
        "ax[1,1].plot(sim_time, force[:,0])\n",
        "ax[1,1].set_yscale('log')\n",
        "ax[1,1].set_title('normal (z) force - log scale')\n",
        "ax[1,1].set_ylabel('Newton')\n",
        "z_gravity = -physics.model.opt.gravity[2]\n",
        "mg = physics.named.model.body_mass[\"box_and_sphere\"] * z_gravity\n",
        "mg_line = ax[1,1].plot(sim_time, np.ones(n_steps)*mg, label='m*g', linewidth=1)\n",
        "ax[1,1].legend()\n",
        "\n",
        "ax[2,1].plot(sim_time, 1000*penetration)\n",
        "ax[2,1].set_title('penetration depth')\n",
        "ax[2,1].set_ylabel('millimeter')\n",
        "ax[2,1].set_xlabel('second')\n",
        "\n",
        "plt.tight_layout()"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "zV5PkYzFXu42"
      },
      "cell_type": "markdown",
      "source": [
        "## Friction"
      ]
    },
    {
      "metadata": {
        "id": "2R_gKoYyXwda"
      },
      "cell_type": "code",
      "source": [
        "#@title tangential friction and slope: {vertical-output: true}\n",
        "MJCF = \"\"\"\n",
        "\u003cmujoco\u003e\n",
        "  \u003casset\u003e\n",
        "    \u003ctexture name=\"grid\" type=\"2d\" builtin=\"checker\" rgb1=\".1 .2 .3\"\n",
        "     rgb2=\".2 .3 .4\" width=\"300\" height=\"300\" mark=\"none\"/\u003e\n",
        "    \u003cmaterial name=\"grid\" texture=\"grid\" texrepeat=\"6 6\"\n",
        "     texuniform=\"true\" reflectance=\".2\"/\u003e\n",
        "     \u003cmaterial name=\"wall\" rgba='.5 .5 .5 1'/\u003e\n",
        "  \u003c/asset\u003e\n",
        "\n",
        "  \u003cdefault\u003e\n",
        "    \u003cgeom type=\"box\" size=\".05 .05 .05\" /\u003e\n",
        "    \u003cjoint type=\"free\"/\u003e\n",
        "  \u003c/default\u003e\n",
        "\n",
        "  \u003cworldbody\u003e\n",
        "    \u003clight name=\"light\" pos=\"-.2 0 1\"/\u003e\n",
        "    \u003cgeom name=\"ground\" type=\"plane\" size=\".5 .5 10\" material=\"grid\"\n",
        "     zaxis=\"-.3 0 1\" friction=\".1\"/\u003e\n",
        "    \u003ccamera name=\"y\" pos=\"-.1 -.6 .3\" xyaxes=\"1 0 0 0 1 2\"/\u003e\n",
        "    \u003cbody pos=\"0 0 .1\"\u003e\n",
        "      \u003cjoint/\u003e\n",
        "      \u003cgeom/\u003e\n",
        "    \u003c/body\u003e\n",
        "    \u003cbody pos=\"0 .2 .1\"\u003e\n",
        "      \u003cjoint/\u003e\n",
        "      \u003cgeom friction=\".33\"/\u003e\n",
        "    \u003c/body\u003e\n",
        "  \u003c/worldbody\u003e\n",
        "\n",
        "\u003c/mujoco\u003e\n",
        "\"\"\"\n",
        "# load\n",
        "physics = mujoco.Physics.from_xml_string(MJCF)\n",
        "\n",
        "n_frames = 60\n",
        "height = 480\n",
        "width = 480\n",
        "video = np.zeros((n_frames, height, width, 3), dtype=np.uint8)\n",
        "\n",
        "# simulate and render\n",
        "physics.reset()\n",
        "for i in range(n_frames):\n",
        "  while physics.data.time \u003c i/30.0:\n",
        "    physics.step()\n",
        "  video[i] = physics.render(height, width, \"y\")\n",
        "display_video(video)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "ArmmaPqGP6W7"
      },
      "cell_type": "markdown",
      "source": [
        "# Actuators and tendons"
      ]
    },
    {
      "metadata": {
        "id": "VJz84c97c8Df"
      },
      "cell_type": "code",
      "source": [
        "#@title bat and piñata: {vertical-output: true}\n",
        "MJCF = \"\"\"\n",
        "\u003cmujoco\u003e\n",
        "  \u003casset\u003e\n",
        "    \u003ctexture name=\"grid\" type=\"2d\" builtin=\"checker\" rgb1=\".1 .2 .3\"\n",
        "     rgb2=\".2 .3 .4\" width=\"300\" height=\"300\" mark=\"none\"/\u003e\n",
        "    \u003cmaterial name=\"grid\" texture=\"grid\" texrepeat=\"1 1\"\n",
        "     texuniform=\"true\" reflectance=\".2\"/\u003e\n",
        "  \u003c/asset\u003e\n",
        "\n",
        "  \u003cworldbody\u003e\n",
        "    \u003clight name=\"light\" pos=\"0 0 1\"/\u003e\n",
        "    \u003cgeom name=\"floor\" type=\"plane\" pos=\"0 0 -.5\" size=\"2 2 .1\" material=\"grid\"/\u003e\n",
        "    \u003csite name=\"anchor\" pos=\"0 0 .3\" size=\".01\"/\u003e\n",
        "    \u003ccamera name=\"fixed\" pos=\"0 -1.3 .5\" xyaxes=\"1 0 0 0 1 2\"/\u003e\n",
        "\n",
        "    \u003cgeom name=\"pole\" type=\"cylinder\" fromto=\".3 0 -.5 .3 0 -.1\" size=\".04\"/\u003e\n",
        "    \u003cbody name=\"bat\" pos=\".3 0 -.1\"\u003e\n",
        "      \u003cjoint name=\"swing\" type=\"hinge\" damping=\"1\" axis=\"0 0 1\"/\u003e\n",
        "      \u003cgeom name=\"bat\" type=\"capsule\" fromto=\"0 0 .04 0 -.3 .04\"\n",
        "       size=\".04\" rgba=\"0 0 1 1\"/\u003e\n",
        "    \u003c/body\u003e\n",
        "\n",
        "    \u003cbody name=\"box_and_sphere\" pos=\"0 0 0\"\u003e\n",
        "      \u003cjoint name=\"free\" type=\"free\"/\u003e\n",
        "      \u003cgeom name=\"red_box\" type=\"box\" size=\".1 .1 .1\" rgba=\"1 0 0 1\"/\u003e\n",
        "      \u003cgeom name=\"green_sphere\"  size=\".06\" pos=\".1 .1 .1\" rgba=\"0 1 0 1\"/\u003e\n",
        "      \u003csite name=\"hook\" pos=\"-.1 -.1 -.1\" size=\".01\"/\u003e\n",
        "    \u003c/body\u003e\n",
        "  \u003c/worldbody\u003e\n",
        "\n",
        "  \u003ctendon\u003e\n",
        "    \u003cspatial name=\"wire\" limited=\"true\" range=\"0 0.35\" width=\"0.003\"\u003e\n",
        "      \u003csite site=\"anchor\"/\u003e\n",
        "      \u003csite site=\"hook\"/\u003e\n",
        "    \u003c/spatial\u003e\n",
        "  \u003c/tendon\u003e\n",
        "\n",
        "  \u003cactuator\u003e\n",
        "    \u003cmotor name=\"my_motor\" joint=\"swing\" gear=\"1\"/\u003e\n",
        "  \u003c/actuator\u003e\n",
        "\u003c/mujoco\u003e\n",
        "\"\"\"\n",
        "physics = mujoco.Physics.from_xml_string(MJCF)\n",
        "PIL.Image.fromarray(physics.render(480, 480, \"fixed\") )\n"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "z-zoBCuBv2Xi"
      },
      "cell_type": "code",
      "source": [
        "#@title actuated bat and passive piñata: {vertical-output: true}\n",
        "n_frames = 180\n",
        "height = 240\n",
        "width = 320\n",
        "video = np.zeros((n_frames, height, width, 3), dtype=np.uint8)\n",
        "\n",
        "# constant actuator signal\n",
        "with physics.reset_context():\n",
        "  physics.named.data.ctrl[\"my_motor\"] = 20\n",
        "\n",
        "# simulate and render\n",
        "for i in range(n_frames):\n",
        "  while physics.data.time \u003c i/30.0:\n",
        "    physics.step()\n",
        "  video[i] = physics.render(height, width, \"fixed\")\n",
        "\n",
        "display_video(video)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "MKNhwpCYjBMw"
      },
      "cell_type": "markdown",
      "source": [
        "Let's ignore the actuator and apply forces directly to the body:"
      ]
    },
    {
      "metadata": {
        "id": "p54vsUOwi_Z8"
      },
      "cell_type": "code",
      "source": [
        "#@title actuated piñata: {vertical-output: true}\n",
        "n_frames = 300\n",
        "height = 240\n",
        "width = 320\n",
        "video = np.zeros((n_frames, height, width, 3), dtype=np.uint8)\n",
        "\n",
        "# constant actuator signal\n",
        "physics.reset()\n",
        "\n",
        "# gravity compensation\n",
        "mg = -(physics.named.model.body_mass[\"box_and_sphere\"] *\n",
        "       physics.model.opt.gravity[2])\n",
        "physics.named.data.xfrc_applied[\"box_and_sphere\", 2] = mg\n",
        "\n",
        "# One Newton in the x direction\n",
        "physics.named.data.xfrc_applied[\"box_and_sphere\", 0] = 1\n",
        "\n",
        "# simulate and render\n",
        "for i in range(n_frames):\n",
        "  while physics.data.time \u003c i/30.0:\n",
        "    physics.step()\n",
        "  video[i] = physics.render(height, width)\n",
        "display_video(video)"
      ],
      "outputs": [],
      "execution_count": null
    },
    {
      "metadata": {
        "id": "viQIvwlvKmaC"
      },
      "cell_type": "markdown",
      "source": [
        "# Kinematic Jacobians\n",
        "A __Jacobian__ is a derivative matrix of a vector-valued function. MuJoCo computes the Jacobians of all transformations between joint space and Cartesian space.\n",
        "\n",
        "Below we use the Jacobian of the end effector position to create a virtual spring to some random target."
      ]
    },
    {
      "metadata": {
        "id": "C9rVZswGMkYu"
      },
      "cell_type": "code",
      "source": [
        "#@title virtual spring-damper: {vertical-output: true}\n",
        "MJCF = \"\"\"\n",
        "\u003cmujoco\u003e\n",
        "  \u003casset\u003e\n",
        "    \u003ctexture name=\"grid\" type=\"2d\" builtin=\"checker\" rgb1=\".1 .2 .3\"\n",
        "     rgb2=\".2 .3 .4\" width=\"300\" height=\"300\" mark=\"none\"/\u003e\n",
        "    \u003cmaterial name=\"grid\" texture=\"grid\" texrepeat=\"6 6\"\n",
        "     texuniform=\"true\" reflectance=\".2\"/\u003e\n",
        "     \u003cmaterial name=\"wall\" rgba='.5 .5 .5 1'/\u003e\n",
        "  \u003c/asset\u003e\n",
        "\n",
        "  \u003coption gravity=\"0 0 0\"\u003e\n",
        "    \u003cflag contact=\"disable\"/\u003e\n",
        "  \u003c/option\u003e\n",
        "\n",
        "  \u003cdefault\u003e\n",
        "    \u003cgeom type=\"capsule\" size=\".02 .02 .02\" /\u003e\n",
        "    \u003cjoint type=\"hinge\" damping=\".02\"/\u003e\n",
        "  \u003c/default\u003e\n",
        "\n",
        "  \u003cworldbody\u003e\n",
        "    \u003clight name=\"light\" pos=\"0 0 1\"/\u003e\n",
        "    \u003cgeom name=\"ground\" type=\"plane\" size=\".5 .5 10\" material=\"grid\"/\u003e\n",
        "    \u003ccamera name=\"y\" pos=\"0 -.8 .6\" xyaxes=\"1 0 0 0 1 2\"/\u003e\n",
        "    \u003ccamera name=\"x\" pos=\"-.8 0 .6\" xyaxes=\"0 -1 0 1 0 2\"/\u003e\n",
        "\n",
        "    \u003cgeom fromto=\"0 0 0 0 0 .2\" /\u003e\n",
        "    \u003cbody pos=\"0 0 .2\"\u003e\n",
        "      \u003cjoint axis=\"0 0 1\"/\u003e\n",
        "      \u003cjoint axis=\"0 1 0\"/\u003e\n",
        "      \u003cgeom fromto=\"0 0 0 .2 0 0\" /\u003e\n",
        "      \u003cbody pos=\".2 0 0\"\u003e\n",
        "        \u003cjoint axis=\"1 0 0\"/\u003e\n",
        "        \u003cjoint axis=\"0 1 0\"/\u003e\n",
        "        \u003cgeom fromto=\"0 0 0 0 0 .15\" /\u003e\n",
        "        \u003cbody pos=\"0 0 .15\"\u003e\n",
        "          \u003cjoint axis=\"0 0 1\"/\u003e\n",
        "          \u003cjoint axis=\"0 1 0\"/\u003e\n",
        "          \u003cgeom fromto=\"0 0 0 .1 0 0\"/\u003e\n",
        "          \u003cgeom name=\"fingertip\" type=\"box\" pos=\".1 0 0\" rgba=\"1 0 0 1\" /\u003e\n",
        "        \u003c/body\u003e\n",
        "      \u003c/body\u003e\n",
        "    \u003c/body\u003e\n",
        "\n",
        "  \u003cgeom name=\"target\" type=\"box\" rgba=\"0 1 0 1\"/\u003e\n",
        "\n",
        "  \u003c/worldbody\u003e\n",
        "\n",
        "\u003c/mujoco\u003e\n",
        "\"\"\"\n",
        "physics = mujoco.Physics.from_xml_string(MJCF)\n",
        "\n",
        "# virtual spring coefficient\n",
        "KP = 3\n",
        "\n",
        "# prepare simulation\n",
        "jac_pos = np.zeros((3, physics.model.nv))\n",
        "jac_rot = np.zeros((3, physics.model.nv))\n",
        "\n",
        "n_frames = 50\n",
        "height = 320\n",
        "width = 320\n",
        "video = np.zeros((n_frames, height, 2*width, 3), dtype=np.uint8)\n",
        "\n",
        "# place target in random location\n",
        "with physics.reset_context():\n",
        "  target_pos = np.random.rand(3)*.5\n",
        "  target_pos[:2] -= .25\n",
        "  physics.named.model.geom_pos[\"target\"][:] = target_pos\n",
        "  physics.named.model.geom_sameframe[\"target\"] = 0\n",
        "\n",
        "# simulate and render\n",
        "for i in range(n_frames):\n",
        "  while physics.data.time \u003c i/15.0:\n",
        "\n",
        "    # get Jacobian of fingertip position\n",
        "    mjlib.mj_jacGeom(physics.model.ptr,\n",
        "                     physics.data.ptr,\n",
        "                     jac_pos,\n",
        "                     jac_rot,\n",
        "                     physics.model.name2id('fingertip', 'geom'))\n",
        "    # multiply the jacobian by error to get vector in joint space\n",
        "    err = (physics.named.data.geom_xpos[\"target\"] -\n",
        "           physics.named.data.geom_xpos[\"fingertip\"])\n",
        "    jnt_err = np.dot(err, jac_pos)\n",
        "\n",
        "    # set virutal spring force\n",
        "    physics.data.qfrc_applied[:] = KP * jnt_err\n",
        "\n",
        "    # step\n",
        "    physics.step()\n",
        "\n",
        "  video[i] = np.hstack((physics.render(height, width, \"y\"),\n",
        "                        physics.render(height, width, \"x\")))\n",
        "\n",
        "display_video(video, framerate=24)"
      ],
      "outputs": [],
      "execution_count": null
    }
  ],
  "metadata": {
    "accelerator": "GPU",
    "colab": {
      "collapsed_sections": [],
      "name": "MuJoCo",
      "private_outputs": true,
      "provenance": []
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
